Popular Searches
Popular Course Categories
Popular Courses

Using Assertions, Test Fixtures, and Mocking Concepts

Using Assertions, Test Fixtures, and Mocking Concepts

Python Testing, Performance, and Practical Projects

Using Assertions, Test Fixtures, and Mocking Concepts in Python

Course: Python Training

Topic: Using Assertions, Test Fixtures, and Mocking Concepts

Course Link: Python Training Course – JustAcademy

Demo Link: Register for a Free Course Demo

JustAcademy Website: Visit JustAcademy


1. Introduction

Assertions, test fixtures, and mocking are important concepts in Python software testing. They help developers verify expected results, prepare reusable test environments, and test code that depends on databases, APIs, file systems, or other external services.

Assertions check whether a result matches an expectation. Test fixtures provide the data and setup required by a test. Mocking replaces a real dependency with a controlled substitute so that a test can run without contacting an actual external system.

These concepts are commonly used with Python's built-in unittest framework and third-party testing tools such as pytest.

2. Learning Objectives

  • Understand assertions and their role in automated testing.
  • Use Python's assert statement and unittest assertion methods.
  • Create test fixtures using setup and teardown methods.
  • Understand pytest fixtures and fixture scopes.
  • Mock functions, API calls, and external dependencies.
  • Use unittest.mock, Mock, and patch.
  • Verify function calls and configure mock return values.
  • Apply assertions, fixtures, and mocking in a practical project.

3. Understanding Assertions

An assertion is a condition that a test expects to be true. If the condition is true, the test continues. If it is false, the assertion fails and the testing framework reports the failure.

3.1 Python's assert Statement

The basic syntax is:

assert condition, "Optional failure message"

Example:

def add(a, b):
    return a + b

result = add(4, 6)

assert result == 10, "Addition result is incorrect"
print("Test passed")

Here, the assertion verifies that the function returns the expected result of 10. If the result differs, Python raises an AssertionError.

3.2 Common Assertion Conditions

Condition Example Purpose
Equality assert actual == expected Checks whether two values are equal.
Inequality assert actual != unexpected Checks whether values differ.
Truth assert is_valid Checks whether a condition is truthy.
False condition assert not is_empty Checks that a condition is false.
Membership assert "Python" in courses Checks whether a value exists in a collection.
Type check assert isinstance(value, int) Checks an object's type or compatible class.
Range check assert 0 <= score <= 100 Checks whether a value falls within a range.

3.3 Testing Functions with Assertions

def calculate_discount(price, percentage):
    if price < 0:
        raise ValueError("Price cannot be negative")
    if not 0 <= percentage <= 100:
        raise ValueError("Percentage must be between 0 and 100")
    return price - (price * percentage / 100)

def test_calculate_discount():
    assert calculate_discount(200, 10) == 180
    assert calculate_discount(100, 0) == 100
    assert calculate_discount(100, 100) == 0

test_calculate_discount()
print("All checks passed")

This example checks a normal discount, a zero-percent discount, and a full discount. Testing multiple meaningful cases helps detect boundary and calculation errors.

3.4 Assertions in pytest

Pytest uses ordinary Python assert statements and provides detailed failure information.

# test_math_operations.py

def multiply(a, b):
    return a * b

def test_multiply_positive_numbers():
    assert multiply(3, 4) == 12

def test_multiply_by_zero():
    assert multiply(9, 0) == 0

def test_multiply_negative_numbers():
    assert multiply(-3, 2) == -6

Save the file with a name beginning with test_, then run:

python -m pip install pytest
python -m pytest -v

3.5 Assertions in unittest

The unittest framework provides assertion methods that give clear test failure messages.

import unittest

def multiply(a, b):
    return a * b

class TestMultiply(unittest.TestCase):
    def test_positive_numbers(self):
        self.assertEqual(multiply(3, 4), 12)

    def test_zero(self):
        self.assertEqual(multiply(5, 0), 0)

    def test_not_equal(self):
        self.assertNotEqual(multiply(2, 3), 5)

if __name__ == "__main__":
    unittest.main()

3.6 Frequently Used unittest Assertion Methods

Method What It Checks
assertEqual(a, b) Both values are equal.
assertNotEqual(a, b) Both values are different.
assertTrue(x) The expression evaluates to true.
assertFalse(x) The expression evaluates to false.
assertIsNone(x) The value is None.
assertIn(a, b) The first value exists in the second collection.
assertIsInstance(obj, cls) The object is an instance of the specified class.
assertRaises(ErrorType) The tested code raises the expected exception.
assertAlmostEqual(a, b) Numeric values are approximately equal.

3.7 Testing Exceptions with Assertions

Tests should verify not only successful results but also how a function behaves with invalid input.

import unittest

def divide(a, b):
    if b == 0:
        raise ZeroDivisionError("Cannot divide by zero")
    return a / b

class TestDivide(unittest.TestCase):
    def test_valid_division(self):
        self.assertEqual(divide(10, 2), 5)

    def test_division_by_zero(self):
        with self.assertRaises(ZeroDivisionError):
            divide(10, 0)

if __name__ == "__main__":
    unittest.main()

In pytest, the equivalent check uses pytest.raises():

import pytest

def test_division_by_zero():
    with pytest.raises(ZeroDivisionError):
        divide(10, 0)

3.8 Important Note About assert

Python's built-in assert statement can be disabled when Python runs with optimization enabled, such as with the -O option. For reliable automated tests, use a testing framework's assertions, such as pytest assertions or unittest.TestCase methods. Avoid using assert as the only mechanism for validating important production input.

4. Understanding Test Fixtures

A test fixture is the setup, data, or environment required to execute a test. For example, a test may need a temporary file, a prepared user object, a database connection, or a configured mock service.

Fixtures help make tests repeatable, organized, and easier to maintain. Instead of repeating the same setup code in every test, developers can define it once and reuse it.

4.1 Typical Fixture Lifecycle

  1. Setup: Prepare test data or required resources.
  2. Execution: Run the function or feature being tested.
  3. Verification: Use assertions to check the result.
  4. Teardown: Release resources or clean up temporary state when necessary.

4.2 Fixtures in unittest

In unittest, setUp() runs before each test method, while tearDown() runs after each test method when the test lifecycle reaches cleanup.

import unittest

class TestShoppingCart(unittest.TestCase):
    def setUp(self):
        self.cart = []
        self.cart.append({"name": "Notebook", "price": 50})

    def tearDown(self):
        self.cart.clear()

    def test_cart_contains_product(self):
        self.assertEqual(len(self.cart), 1)
        self.assertEqual(self.cart[0]["name"], "Notebook")

    def test_product_price(self):
        self.assertEqual(self.cart[0]["price"], 50)

if __name__ == "__main__":
    unittest.main()

Each test receives a newly initialized cart, preventing changes made by one test from accidentally affecting another.

4.3 Class-Level Fixtures in unittest

Use setUpClass() and tearDownClass() for resources that can safely be shared by all test methods in a class.

import unittest

class TestConfiguration(unittest.TestCase):
    @classmethod
    def setUpClass(cls):
        cls.settings = {
            "environment": "testing",
            "debug": True
        }

    @classmethod
    def tearDownClass(cls):
        cls.settings = None

    def test_environment(self):
        self.assertEqual(self.settings["environment"], "testing")

    def test_debug_mode(self):
        self.assertTrue(self.settings["debug"])

if __name__ == "__main__":
    unittest.main()

Class-level setup runs once for the test class rather than once for every test method. Use it carefully because shared mutable state can cause tests to interfere with each other.

4.4 pytest Fixtures

Pytest provides the @pytest.fixture decorator to define reusable setup functions. A test requests a fixture by including its name as a function parameter.

import pytest

@pytest.fixture
def sample_user():
    return {
        "id": 101,
        "name": "Asha",
        "active": True
    }

def test_user_name(sample_user):
    assert sample_user["name"] == "Asha"

def test_user_is_active(sample_user):
    assert sample_user["active"] is True

Pytest automatically runs the fixture and passes its returned value to each test that requests it.

4.5 Fixture Scopes in pytest

Scope Lifetime Typical Use
function Once per test function; the default. Fresh test data or isolated temporary objects.
class Once per test class. Resources shared within one class.
module Once per Python test module. Expensive setup shared within a file.
package Once per package, according to pytest's fixture lifecycle. Resources shared across tests in a package.
session Once per test session. Session-wide configuration or expensive resources.

Example of a module-scoped fixture:

import pytest

@pytest.fixture(scope="module")
def application_config():
    return {
        "mode": "testing",
        "timeout": 10
    }

def test_mode(application_config):
    assert application_config["mode"] == "testing"

def test_timeout(application_config):
    assert application_config["timeout"] == 10

4.6 Fixture Cleanup with yield

A pytest fixture can use yield to separate setup from cleanup. The statements after yield run during fixture teardown.

import pytest

@pytest.fixture
def temporary_resource():
    resource = {"connected": True}
    yield resource
    resource["connected"] = False

def test_resource_connection(temporary_resource):
    assert temporary_resource["connected"] is True

For real resources, the cleanup section can close a connection, remove a temporary file, or release another resource. Cleanup should be designed so that it runs reliably when the fixture is torn down.

5. Understanding Mocking

Mocking replaces a real object or dependency with a controllable test double. It allows a test to define return values, simulate errors, and check whether a dependency was called correctly.

For example, an application may request weather data from an external API. A unit test should usually verify the application's behavior without depending on the network, the API's availability, or a real API key. A mock can simulate the API response.

5.1 Why Mocking Is Useful

  • Isolation: Test one unit without relying on unrelated components.
  • Speed: Avoid slow network calls or expensive database operations.
  • Reliability: Reduce failures caused by external service outages.
  • Controlled scenarios: Simulate success, empty responses, and exceptions.
  • Call verification: Check whether a dependency received the expected arguments.
  • Cost control: Avoid unnecessary calls to paid APIs during tests.

5.2 The unittest.mock Module

Python includes unittest.mock in its standard library. Common tools include:

Tool Purpose
Mock Creates an object whose behavior and calls can be configured.
MagicMock A mock with support for many special methods, such as iteration and item access.
patch() Temporarily replaces an object in the namespace where the code under test looks it up.
return_value Defines the value returned when a mock is called.
side_effect Can raise an exception, run a function, or return different values across calls.
assert_called_once_with() Verifies that a mock was called exactly once with specified arguments.

5.3 Creating a Basic Mock

from unittest.mock import Mock

payment_service = Mock()
payment_service.charge.return_value = {
    "status": "success",
    "transaction_id": "TXN-101"
}

result = payment_service.charge(500)

assert result["status"] == "success"
payment_service.charge.assert_called_once_with(500)

The test controls the payment service's response and checks that the application called it with the expected amount. No real payment is made.

5.4 Mocking a Function with patch

The patch() function temporarily replaces a dependency during a test. Patch the name where the code under test looks it up, rather than automatically patching the place where the object was originally defined.

Suppose the application module is named weather_app.py:

# weather_app.py
import requests

def get_temperature(city):
    response = requests.get(
        f"https://example.com/weather?city={city}",
        timeout=5
    )
    response.raise_for_status()
    return response.json()["temperature"]

A test can mock the request so that no real network call is made:

# test_weather_app.py
from unittest.mock import Mock, patch
from weather_app import get_temperature

@patch("weather_app.requests.get")
def test_get_temperature(mock_get):
    mock_response = Mock()
    mock_response.json.return_value = {"temperature": 28}
    mock_response.raise_for_status.return_value = None
    mock_get.return_value = mock_response

    result = get_temperature("Mumbai")

    assert result == 28
    mock_get.assert_called_once_with(
        "https://example.com/weather?city=Mumbai",
        timeout=5
    )

In this example, weather_app.requests.get is patched because that is the name used by the module under test. The mock response supplies predictable JSON, and the assertion verifies both the result and the request arguments.

5.5 Using return_value

Use return_value when a mocked function should return a fixed value.

from unittest.mock import Mock

get_username = Mock(return_value="Asha")

username = get_username(101)

assert username == "Asha"
get_username.assert_called_once_with(101)

5.6 Using side_effect

The side_effect property can simulate exceptions or changing responses.

Example: testing an error from a service:

from unittest.mock import Mock
import unittest

def load_profile(service, user_id):
    return service.fetch(user_id)

class TestProfile(unittest.TestCase):
    def test_service_error(self):
        service = Mock()
        service.fetch.side_effect = ConnectionError("Service unavailable")

        with self.assertRaises(ConnectionError):
            load_profile(service, 101)

if __name__ == "__main__":
    unittest.main()

Example: returning a sequence of values:

from unittest.mock import Mock

fetch_status = Mock(side_effect=["pending", "complete"])

assert fetch_status() == "pending"
assert fetch_status() == "complete"

5.7 Verifying Mock Calls

from unittest.mock import Mock

logger = Mock()

logger.info("User logged in")
logger.info("Report generated")

assert logger.info.call_count == 2
logger.info.assert_any_call("User logged in")

Mock call checks are useful when a function's side effect is important, such as sending a notification, writing a log entry, or calling a payment gateway.

6. Combining Assertions, Fixtures, and Mocking

The three concepts work together to make automated tests more effective:

  • Assertions verify the output or behavior.
  • Fixtures prepare reusable test data and resources.
  • Mocks replace external dependencies and simulate their behavior.

Consider an order service that calculates an order total and calls a payment gateway. A test can prepare an order using a fixture, mock the payment gateway, and assert that the correct total was charged.

# order_service.py
def calculate_total(items):
    return sum(item["price"] * item["quantity"] for item in items)

def checkout(items, payment_gateway):
    total = calculate_total(items)
    payment_result = payment_gateway.charge(total)
    return {
        "total": total,
        "payment_status": payment_result["status"]
    }

Test example using pytest:

# test_order_service.py
import pytest
from unittest.mock import Mock
from order_service import checkout

@pytest.fixture
def sample_items():
    return [
        {"name": "Book", "price": 200, "quantity": 2},
        {"name": "Pen", "price": 20, "quantity": 3}
    ]

@pytest.fixture
def payment_gateway():
    gateway = Mock()
    gateway.charge.return_value = {"status": "success"}
    return gateway

def test_checkout(sample_items, payment_gateway):
    result = checkout(sample_items, payment_gateway)

    assert result["total"] == 460
    assert result["payment_status"] == "success"
    payment_gateway.charge.assert_called_once_with(460)

This test does not contact a real payment provider. The fixture supplies predictable products, the mock simulates payment, and the assertions verify the total, payment status, and gateway call.

7. Mocking External Services Safely

When testing code that uses external services, remember these guidelines:

  • Mock the dependency at the point where the tested module looks it up.
  • Configure return values to match the structure the application expects.
  • Test both successful responses and relevant failure cases.
  • Verify important arguments, such as IDs, request payloads, and timeout values.
  • Do not mock every function. Keep simple business logic real whenever practical.
  • Use integration tests separately when you need to verify that components work together against a real service or test environment.

8. Common Mistakes and Troubleshooting

Problem Likely Cause Recommended Solution
Assertion fails unexpectedly The actual result differs from the expected result or the expected value is incorrect. Inspect inputs, return values, and the intended behavior.
Fixture is not found The fixture is missing, misspelled, or not available in the test's scope. Check the fixture name and where it is defined.
Mock does not replace the dependency The wrong import path was patched. Patch the name used by the module under test.
Mock returns an unexpected object The mock's return value or nested method was not configured. Configure the expected return value and response attributes explicitly.
Tests influence each other Mutable data or resources are shared across tests. Use function-scoped fixtures or reset shared state carefully.
Mock call assertion fails The function was called with different arguments or a different number of times. Inspect the call arguments and confirm the intended behavior.
Tests pass but production code fails The mock does not accurately represent the real dependency. Add integration tests and keep mock behavior realistic.

9. Best Practices

  • Write tests with clear names that describe the behavior being verified.
  • Test normal inputs, boundary values, and expected errors.
  • Use assertions that clearly express the expected outcome.
  • Keep each test focused on one main behavior.
  • Use fixtures to remove repetitive setup code without hiding important test details.
  • Prefer fresh test data when isolation matters.
  • Mock external systems that are slow, costly, unstable, or outside the unit's responsibility.
  • Verify mock calls only when the interaction itself is meaningful.
  • Avoid excessive mocking that duplicates the implementation instead of testing its behavior.
  • Run tests regularly and include them in continuous integration where appropriate.
  • Use descriptive failure messages when they improve the clarity of custom checks.
  • Keep secrets, real payment details, and production credentials out of test code.

10. Practice Exercises

Exercise 1: Assertions

Create a function named is_even(number) that returns True for even numbers and False for odd numbers. Write tests for positive, negative, and zero inputs.

Exercise 2: Test Fixtures

Create a pytest fixture that returns three product dictionaries. Write tests that calculate the number of products and verify that each product has a name and price.

Exercise 3: Mocking

Create a function that calls a notification service. Mock the service and verify that it receives the expected recipient and message without sending a real notification.

Exercise 4: Exception Testing

Write a function that validates an age value. Test valid ages and use pytest.raises(ValueError) to check invalid inputs.

Exercise 5: Combined Testing

Write an order-processing function that calculates a total and calls a shipping service. Use fixtures for sample orders, a mock for the shipping service, and assertions to check the total and shipping call.

11. Mini Project: Testing an Order Service

Project goal: Create a small order service and test it using assertions, fixtures, and mocking.

Requirements

  • Accept a list of products with prices and quantities.
  • Calculate the total order amount.
  • Call a mock payment gateway to process the amount.
  • Return the total and payment status.
  • Test successful payment and payment failure.
  • Verify that the payment gateway is called with the correct total.

Sample Implementation

# shop.py
def calculate_total(items):
    if not items:
        return 0

    for item in items:
        if item["price"] < 0 or item["quantity"] < 0:
            raise ValueError("Price and quantity cannot be negative")

    return sum(
        item["price"] * item["quantity"]
        for item in items
    )

def process_order(items, gateway):
    total = calculate_total(items)
    response = gateway.charge(total)

    return {
        "total": total,
        "status": response["status"]
    }

Sample Tests

# test_shop.py
import pytest
from unittest.mock import Mock
from shop import calculate_total, process_order

@pytest.fixture
def items():
    return [
        {"price": 100, "quantity": 2},
        {"price": 50, "quantity": 1}
    ]

def test_calculate_total(items):
    assert calculate_total(items) == 250

def test_empty_order():
    assert calculate_total([]) == 0

def test_negative_price():
    with pytest.raises(ValueError):
        calculate_total([{"price": -10, "quantity": 1}])

def test_process_order(items):
    gateway = Mock()
    gateway.charge.return_value = {"status": "success"}

    result = process_order(items, gateway)

    assert result == {"total": 250, "status": "success"}
    gateway.charge.assert_called_once_with(250)

def test_payment_failure(items):
    gateway = Mock()
    gateway.charge.return_value = {"status": "failed"}

    result = process_order(items, gateway)

    assert result["status"] == "failed"

Run the tests using:

python -m pip install pytest
python -m pytest -v

This project demonstrates how fixtures provide repeatable input data, assertions verify business logic, and mocks simulate payment responses without making real transactions.

12. Interview Questions and Answers

Q1. What is an assertion in Python testing?

An assertion checks whether a condition or result matches an expectation. If the condition is false, the assertion reports a failure.

Q2. What is the difference between Python's assert statement and unittest assertions?

Python's assert statement checks a condition directly but can be disabled under optimized execution. Methods such as assertEqual() and assertRaises() are provided by the unittest framework and are intended for structured test cases.

Q3. What is a test fixture?

A test fixture is reusable setup or test data required for a test. It can prepare objects, configuration, temporary files, or other resources.

Q4. What is the difference between setUp() and setUpClass()?

setUp() runs before each test method in a unittest test case. setUpClass() runs once for the class and must be defined as a class method.

Q5. What is a pytest fixture?

A pytest fixture is a function marked with @pytest.fixture that provides reusable data or resources to tests that request it by name.

Q6. What is mocking?

Mocking replaces a dependency with a controllable test double so that a test can simulate responses, errors, and interactions without relying on the real dependency.

Q7. What is the purpose of patch()?

patch() temporarily replaces an object in a specified namespace during a test. It is commonly used to replace network calls, database access, or other external dependencies.

Q8. What is the difference between return_value and side_effect?

return_value defines what a mock returns when called. side_effect can raise an exception, call a custom function, or provide a sequence of responses.

Q9. Why should external API calls often be mocked in unit tests?

Mocking avoids network dependency, improves test speed and reliability, and makes it possible to simulate responses and errors consistently.

Q10. What is the difference between unit tests and integration tests?

Unit tests check an individual component in isolation, often using mocks. Integration tests check whether multiple components work together, sometimes using real test databases or services.

13. Quick Revision Checklist

  • Assertions verify expected results and behavior.
  • Pytest uses Python's assert statement and provides detailed failure output.
  • Unittest provides assertion methods such as assertEqual() and assertRaises().
  • Fixtures prepare reusable data and resources.
  • setUp() and tearDown() support unittest test lifecycle management.
  • @pytest.fixture defines fixtures in pytest.
  • Fixture scopes control how long fixture results are reused.
  • Mock and patch() help replace external dependencies.
  • return_value configures a mock's result.
  • side_effect simulates exceptions or changing results.
  • Mock call assertions verify important interactions.
  • Well-designed tests should be isolated, readable, repeatable, and focused on behavior.

14. Conclusion

Assertions, test fixtures, and mocking are foundational techniques for building reliable Python tests. Assertions verify that code behaves as expected, fixtures prepare consistent test environments, and mocks allow external dependencies to be controlled. Together, they make it easier to test successful operations, boundary conditions, and failure scenarios without unnecessary dependence on real external systems.

Practice these techniques using both unittest and pytest to develop a strong foundation in Python testing and maintainable software development.


Learn More with JustAcademy

Explore the Python Training Course for additional Python learning, or register for a course demo to learn more about the training options. You can also visit the JustAcademy website.

whatsapp